package me.huanmeng.heater.plugin;

import cn.hutool.core.io.FileUtil;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import com.google.common.graph.GraphBuilder;
import com.google.common.graph.MutableGraph;
import me.huanmeng.heater.Heater;
import me.huanmeng.heater.bot.Bot;
import me.huanmeng.heater.bot.MessageHelper;
import me.huanmeng.heater.exception.PluginLoadException;
import me.huanmeng.heater.heather.ServerImpl;
import org.jetbrains.annotations.NotNull;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.logging.Level;

/**
 * 2021/9/30<br>
 * Heater<br>
 *
 * @author huanmeng_qwq
 */
public class PluginManager implements MessageHelper {
    private final Bot bot;
    private final List<PluginClassLoader> loaders = new ArrayList<>();
    private final List<Plugin> plugins = new ArrayList<>();
    private Map<String, File> pluginFiles = new HashMap<>();
    private final Map<String, Plugin> lookupNames = new HashMap<>();

    public PluginManager(Bot bot) {
        this.bot = bot;
    }

    public PluginDescription getPluginDescription(@NotNull File file) throws PluginLoadException {
        try {
            final JarFile jarFile = new JarFile(file);
            JarEntry jarEntry = jarFile.getJarEntry("heater.yml");
            if (jarEntry == null) {
                jarEntry = jarFile.getJarEntry("plugin.yml");
            }
            if (jarEntry == null) {
                throw new PluginLoadException(new FileNotFoundException("Jar does not contain plugin.yml"));
            }
            return new PluginDescription(jarFile.getInputStream(jarEntry));
        } catch (Exception e) {
            throw new PluginLoadException(e);
        }
    }

    public List<Plugin> loadPlugins(File folder) {
        if (!folder.exists()) {
            folder.mkdirs();
        }
        log(Level.INFO, "Start load plugins.");
        List<File> pluginFiles = new ArrayList<>();
        Multimap<File, String> dependencies = HashMultimap.create();
        Multimap<File, String> softDependencies = HashMultimap.create();
        MutableGraph<File> sort = GraphBuilder.directed().build();
        for (File file : FileUtil.loopFiles(folder, pathname -> pathname.getName().endsWith(".jar"))) {
            try {
                //符合要求的插件
                final PluginDescription description;
                description = getPluginDescription(file);
                final String name = description.getName();
                if (name.equalsIgnoreCase("heater")) {
                    log(Level.SEVERE, String.format("plugin %s name is invalid.", description.getFullName()));
                    continue;
                } else if (name.contains(" ")) {
                    log(Level.SEVERE, String.format("plugin %s name is invalid.", description.getFullName()));
                    continue;
                } else if (!description.getSupportApiVersions().isEmpty()) {
                    if (!((ServerImpl) Heater.getServer()).allowSupport(description.getSupportApiVersions())) {
                        log(Level.SEVERE, String.format("Plugin %s not support the server version: %s", description.getFullName(), Heater.getVersion()));
                        continue;
                    }
                }
                this.pluginFiles.put(description.getName(), file);
                pluginFiles.add(file);
                dependencies.putAll(file, description.getDepends());
                softDependencies.putAll(file, description.getSoftDepends());
            } catch (Exception e) {
                log(Level.SEVERE, "Load '" + file.getPath() + "' in folder '" + folder.getPath() + "' failed.", e);
            }
        }
        try {
            for (File file : dependencies.keySet()) {
                final Collection<String> dep = dependencies.get(file);
                for (String s : dep) {
                    if (!this.pluginFiles.containsKey(s)) {
                        dependencies.removeAll(file);
                        softDependencies.removeAll(file);
                        pluginFiles.remove(file);
                        log(Level.SEVERE, "Load '" + file.getPath() + "' it need "+s+" plugin,please install it first.");
                        break;
                    } else {
                        sort.putEdge(file, this.pluginFiles.get(s));
                    }
                }
            }
        } catch (Exception e) {
            log(Level.SEVERE, e);
        } finally {
            cleanup(pluginFiles);
        }
        List<Plugin> plugins = new ArrayList<>();
        try {
            for (File file : pluginFiles) {
                final Plugin plugin = loadPlugin(file);
                if (null != plugin) {
                    plugins.add(plugin);
                } else {
                    dependencies.removeAll(file);
                    softDependencies.removeAll(file);
                    pluginFiles.remove(file);
                    log(Level.SEVERE, "Load '" + file.getPath() + "' in folder '" + folder.getPath() + "' failed.");
                }
            }
        } catch (Exception e) {
            log(Level.SEVERE, e);
        } finally {
            cleanup(pluginFiles);
        }
        return plugins;
    }

    final void cleanup(List<File> pluginFiles) {
        final HashMap<String, File> map = new HashMap<>(this.pluginFiles);
        for (Map.Entry<String, File> entry : this.pluginFiles.entrySet()) {
            if (!pluginFiles.contains(entry.getValue())) {
                map.remove(entry.getKey());
            }
        }
        this.pluginFiles = map;
    }

    public Plugin loadPlugin(File file) throws PluginLoadException {
        try {
            final PluginDescription pluginDescription = getPluginDescription(file);
            final File folder = file.getParentFile();
            final File dataFolder = new File(folder, pluginDescription.getName());
            if (dataFolder.exists() && !dataFolder.isDirectory()) {
                throw new PluginLoadException(String.format("%s data folder is not directory.", pluginDescription.getFullName()));
            }
            for (String depend : pluginDescription.getDepends()) {
                final Plugin plugin = getPlugin(depend);
                if (plugin == null) {
                    throw new PluginLoadException(String.format("Unknown dependency %s,Please install %s and restart.", depend, depend));
                }
            }
            final PluginClassLoader loader = new PluginClassLoader(file, dataFolder, pluginDescription, getClass().getClassLoader());
            loaders.add(loader);
            final JavaPlugin javaPlugin = loader.plugin;
            String message = String.format("Loaded %s", javaPlugin.getDescription().getFullName());
            javaPlugin.onLoad();
            log(Level.INFO, message);
            lookupNames.put(pluginDescription.getName(), javaPlugin);
            plugins.add(javaPlugin);
            return javaPlugin;
        } catch (Exception e) {
            throw new PluginLoadException(e);
        }
    }

    public Plugin getPlugin(String name) {
        return lookupNames.get(name.replaceAll(" ", "_"));
    }

    public List<Plugin> getPlugins() {
        return Collections.unmodifiableList(plugins);
    }

    public void disablePlugin(Plugin plugin) {
        String message = String.format("Disabled %s", plugin.getDescription().getFullName());
        if (plugin.isEnabled()) {
            plugin.setEnabled(false);
            plugin.onDisable();
            final ClassLoader classLoader = ((JavaPlugin) plugin).getLoader();
            if (classLoader instanceof PluginClassLoader) {
                loaders.remove(classLoader);
                try {
                    ((PluginClassLoader) classLoader).close();
                } catch (IOException ignored) {
                }
            }
            log(Level.INFO, message);
            this.plugins.remove(plugin);
            this.pluginFiles.remove(plugin.getDescription().getName());
            this.lookupNames.remove(plugin.getDescription().getName());
        }
    }

    public void enablePlugin(Plugin plugin) {
        if (plugin.isEnabled()) {
            return;
        }
        String message = String.format("Enabled %s", plugin.getDescription().getFullName());
        plugin.setEnabled(true);
        plugin.onEnable();
        log(Level.INFO, message);
    }
}
